package cn.dansj.common.utils.jdbc.mybatis;

import cn.dansj.common.utils.transfer.Verification;
import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.scripting.xmltags.XMLScriptBuilder;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.apache.ibatis.type.TypeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;

/**
 * 使用Mybatis的XML解析来进行SQL构建的执行器
 */
@SuppressWarnings("all")
public class MybatisXmlParser {

    /**
     * 脚本模板
     */
    private static final String SCRIPT_TEMPLATE = "<script>\n%s\n</script>";
    private static final Logger logger = LoggerFactory.getLogger(MybatisXmlParser.class);

    /**
     * MP环境配置
     */
    private final Configuration configuration;

    /**
     *
     */
    private static final String DruidPooledPreparedStatement = "com.alibaba.druid.pool.DruidPooledPreparedStatement";
    private static final String T4CPreparedStatement = "oracle.jdbc.driver.T4CPreparedStatement";
    private static final String OraclePreparedStatementWrapper = "oracle.jdbc.driver.OraclePreparedStatementWrapper";
    private Method oracleGetOriginalSqlMethod;
    private Method druidGetSQLMethod;

    /**
     * 构造器，初始化环境配置
     */
    public MybatisXmlParser() {
        InputStream inputStream = new ByteArrayInputStream(EMPTY_XML.getBytes(StandardCharsets.UTF_8));
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(inputStream, null, null);
        configuration = xmlConfigBuilder.parse();
    }

    public String parseSql(Connection connection, String sqlTemplate, Map<?, ?> params) throws SQLException {
        try {
            String script = String.format(SCRIPT_TEMPLATE, sqlTemplate);
            XPathParser parser = new XPathParser(script, false, new Properties(), new XMLMapperEntityResolver());
            SqlSource source = createSqlSource(configuration, parser.evalNode("/script"), Map.class);
            BoundSql boundSql = source.getBoundSql(params);
            LoggableStatement statement = buildPreparedStatement(connection, boundSql);
            String originalSql = getOriginSql(statement.getPreparedStatement(boundSql.getSql()));
            int index = indexOfSqlStart(originalSql);
            if (index > 0) {
                originalSql = originalSql.substring(index);
            }
            if (!isCorrectGetSql(boundSql, originalSql)) {
                originalSql = statement.getQueryString();
            }
            return originalSql;
        } catch (Exception e) {
            throw new SQLException(e);
        } finally {
            connection.close();
        }
    }

    public String parseSql(JdbcTemplate jdbcTemplate, String sqlTemplate, Map<?, ?> params) throws SQLException {
        return parseSql(jdbcTemplate.getDataSource().getConnection(), sqlTemplate, params);
    }

    /**
     * 从MP复制过来的脚本解析方法
     */
    @SuppressWarnings("all")
    private SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

    /**
     * 根据BoundSql组装PreparedStatement，用于获取实际SQL
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    private LoggableStatement buildPreparedStatement(Connection connection, BoundSql boundSql) throws SQLException {
        PreparedStatement ps = connection.prepareStatement(boundSql.getSql());
        LoggableStatement ls = new LoggableStatement(connection, boundSql.getSql());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();
        if (parameterMappings != null) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    JdbcType jdbcType = parameterMapping.getJdbcType();
                    if (value == null && jdbcType == null) {
                        jdbcType = configuration.getJdbcTypeForNull();
                    }
                    try {
                        typeHandler.setParameter(ls, i + 1, value, jdbcType);
                    } catch (TypeException | SQLException e) {
                        throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                    }
                }
            }
        }

        return ls;
    }

    /**
     * 获取sql语句开头部分
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (Verification.checkNull(set)) return -1;
        List<Integer> list = new ArrayList<>(set);
        list.sort(Comparator.naturalOrder());
        return list.get(0);
    }

    /**
     *
     */
    private String getOriginSql(PreparedStatement statement) {
        String originalSql = null;
        String stmtClassName = statement.getClass().getName();
        if (DruidPooledPreparedStatement.equals(stmtClassName)) {
            try {
                if (druidGetSQLMethod == null) {
                    Class<?> clazz = Class.forName(DruidPooledPreparedStatement);
                    druidGetSQLMethod = clazz.getMethod("getSql");
                }
                Object stmtSql = druidGetSQLMethod.invoke(statement);
                if (stmtSql instanceof String) {
                    originalSql = (String) stmtSql;
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        } else if (T4CPreparedStatement.equals(stmtClassName) || OraclePreparedStatementWrapper.equals(stmtClassName)) {
            try {
                if (oracleGetOriginalSqlMethod != null) {
                    Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement);
                    if (stmtSql instanceof String) {
                        originalSql = (String) stmtSql;
                    }
                } else {
                    Class<?> clazz = Class.forName(stmtClassName);
                    oracleGetOriginalSqlMethod = getMethodRegular(clazz, "getOriginalSql");
                    if (oracleGetOriginalSqlMethod != null) {
                        // OraclePreparedStatementWrapper is not a public class, need set this.
                        oracleGetOriginalSqlMethod.setAccessible(true);
                        if (null != oracleGetOriginalSqlMethod) {
                            Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement);
                            if (stmtSql instanceof String) {
                                originalSql = (String) stmtSql;
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // ignore
            }
        }
        if (originalSql == null) {
            originalSql = statement.toString();
        }

        return originalSql;
    }

    /**
     * 获取此方法名的具体 Method
     *
     * @param clazz      class 对象
     * @param methodName 方法名
     * @return 方法
     */
    private Method getMethodRegular(Class<?> clazz, String methodName) {
        if (Object.class.equals(clazz)) {
            return null;
        }
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(methodName)) {
                return method;
            }
        }
        return getMethodRegular(clazz.getSuperclass(), methodName);
    }

    /**
     * 判断是否正确的获取了SQL
     */
    private boolean isCorrectGetSql(BoundSql boundSql, String originSql) {
        return countQuestionMark(boundSql.getSql()) > countQuestionMark(originSql);
    }

    /**
     * 统计占位符
     */
    private int countQuestionMark(String sql) {
        int result = 0;
        for (char c : sql.toCharArray())
            if (c == '?')
                result++;
        return result;
    }

    /**
     * 空MP配置模板，用于构建MP环境配置
     */
    private static final String EMPTY_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
            "<!DOCTYPE configuration PUBLIC \"-//mybatis.org//DTD Config 3.0//EN\"  \"http://mybatis.org/dtd/mybatis-3-config.dtd\">\n" +
            "<configuration>\n" +
            "</configuration>";
}
